fix(security): authenticate extension services - #2638
Conversation
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This should become an extension SDK package. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
6763e56 to
4dc58b4
Compare
|
/ok to test 4dc58b4 |
|
🌿 Preview your docs: https://nvidia-preview-pr-2638.docs.buildwithfern.com/openshell |
Follow-up hardening on the alpha extension authentication mechanism. Claim contract: - Extension tokens carry an explicit `typ` of `openshell-ext+jwt`. They share a signing key with sandbox-to-gateway admission tokens and were otherwise separated by audience alone, so a verifier that neglects to check `aud` could accept a gateway credential. The header is a second, independent discriminator. - Publish OIDC-shaped discovery at `/.well-known/openid-configuration` so a service configured with only the gateway URL can learn the exact expected issuer and the JWKS location. It is shaped, not compliant: `issuer` is the gateway identity, not the serving URL. Audience agreement: - `MiddlewareManifest` and `InterceptorManifest` gain `expected_audience`. The audience is otherwise configured independently on each side of the boundary, where a mismatch surfaces only as an opaque authentication failure on every call. OpenShell now compares the two and fails at startup. An empty field keeps the check off for existing services. Compatibility: - Add `allow_insecure_transport` per registration. Enabling gateway JWT signing previously made any plaintext endpoint a hard startup failure, including the endpoint form used in our own documentation. The opt-out attaches no credential, is refused by the gateway if a supervisor asks for one, and warns at every startup. - Make the transport requirement kind-aware. A middleware endpoint must be reachable from every sandbox supervisor, so only interceptors may use a gateway-local Unix socket. Credential lifecycle: - Replace the process-global slot map with a supervisor-owned `ExtensionCredentialStore` shared explicitly across the gateway connections the supervisor opens, removing test-order coupling. - Rotate only when a credential is missing or has passed four fifths of its lifetime. Configuration polling ran every ten seconds against fifteen-minute credentials, so each poll re-ran gateway effective-policy resolution and re-minted the gateway token. - Bound credential minting per sandbox, since each request resolves the caller's effective policy. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Restore the RFC 0009 and 0010 bodies to their accepted text and move every extension-authentication update into appendices instead. An RFC records a decision at a point in time; superseding detail belongs alongside it rather than rewritten into it. RFC 0009's appendix carries the shared contract: claims, authorization, key distribution, the `allow_insecure_transport` replacement for the body's `allow_insecure`, and residual risks. RFC 0010's records only what differs for interceptors and links to it. The existing protocol-extensions appendix, which parked the phase 2 transport question, now points forward to what was built. Also document the audience handshake, the discovery endpoint, the `typ` requirement, and `jti` replay guidance in the extensibility and gateway configuration pages, and correct the middleware transport guidance: middleware endpoints must be reachable from sandbox supervisors, so Unix sockets are not an option there. Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
|
/ok to test 32f93e1 |
|
On the JWKS bootstrap follow-up: operator provisioning the key seems the better default. Binding the listener early to serve only well-known paths adds a readiness state whose only job is to hand out material an out-of-band channel already has to deliver for a cold start — the first-contact service still needs something before the gateway is reachable. Treating the certgen-written key as the supported bootstrap keeps one trust path instead of two. The well-known endpoints are still worth keeping as the steady-state convenience for services that can already reach the gateway; the question is only which one is authoritative for first contact. |
Summary
Authenticate remote supervisor middleware and gateway-interceptor RPCs with short-lived, exact-audience gateway-signed JWTs. Add a shared extension client foundation so both mechanisms use the same bearer rotation and TLS/custom-CA implementation.
Related Issue
Closes #2623
Security Fix
Remote extension services previously could not cryptographically distinguish OpenShell gateway or sandbox-supervisor calls from direct network callers. This change adds authenticated caller identity and policy-constrained token distribution while preserving the existing Ed25519 signing authority and sandbox refresh API.
Severity Assessment
Changes
openshell-extension-core, a shared foundation for extension identity, claims, bearer rotation, and TLS/custom-CA transport used by both supervisor middleware and gateway interceptors.RefreshSandboxToken, held in a supervisor-owned store that rotates slots in place./.well-known/jwks.jsonand/.well-known/openid-configuration.Describemanifest at startup rather than relying on configuration matching on both sides of the boundary.allow_insecure_transportper registration for deployments that keep a plaintext endpoint.Review notes
Compatibility. With
gateway_jwtconfigured, extension endpoints usehttps://; interceptors may also useunix://, middleware may not, because a middleware endpoint has to be reachable from every sandbox supervisor. Deployments that keep a plaintext endpoint setallow_insecure_transport = trueon that registration. OpenShell then attaches no credential, refuses to mint one if a supervisor asks, and names the registration in a warning at every gateway startup.Rotation cadence. Configuration polling runs every 10s while credentials last 15 minutes. Supervisors reuse their installed slots and rotate only when one is missing or has passed four fifths of its lifetime, so a routine poll does not re-run gateway effective-policy resolution or re-mint the gateway token.
Authorization resolution.
handle_get_sandbox_configis the single resolver deciding which registrations a sandbox may hold credentials for. A cheaper parallel resolver would duplicate ~120 lines of policy composition and backfill side effects on a security-critical path, where drift fails open.RFC layout. Extension-authentication detail lives in
appendices/extension-authentication.mdunder RFC 0009 (the shared contract) and RFC 0010 (interceptor differences), leaving both accepted RFC bodies intact. RFC 0010's appendix is currently reachable only by path, since its body has no appendix reference to link from.Out of scope: the service-side verification SDK (tracked separately; Rust and Python first), a separate signing key per trust domain, and mTLS. Residual risks are recorded in the RFC 0009 appendix.
Testing
mise run pre-commitpassesChecklist
Example branch
The runnable authenticated middleware example is intentionally kept on the separate
2623-authenticated-middleware-example/pimlockbranch. Its service-side JWT verifier is a dedicated example module marked for future extraction into an extension-building SDK, keeping inbound verification concerns out ofopenshell-extension-core. After this PR lands, the branch can be rebased and merged separately or folded into later work. Its verifier must requiretyp: openshell-ext+jwtand returnexpected_audiencefromDescribe.Follow-up: JWKS bootstrap
Verification keys are served once the gateway is up, so a service's first contact still depends on out-of-band provisioning.
/.well-known/openid-configurationcovers discovery for services that can reach the gateway. The open question is whether to bind the primary listener early and serve only the well-known routes until ready, or to treat operator provisioning of the public key that certgen already writes to disk as the supported bootstrap and leave both endpoints as convenience. The latter matches how Kubernetes handles the same problem and avoids the listener-swap machinery.